♊ fix: Sanitize MCP Tool Schemas for Gemini/Vertex Compatibility#13623
Conversation
|
@codex review |
There was a problem hiding this comment.
Pull request overview
This PR fixes Gemini/Vertex AI agent crashes when MCP tools provide union-typed JSON parameter schemas (anyOf/oneOf or multi-type type arrays) by flattening those unions only on the Google/Vertex tool-schema path, preserving existing behavior for OpenAI/Claude.
Changes:
- Added
flattenJsonSchemaUnions()(Gemini-specific, intentionally lossy) to collapseanyOf/oneOfand multi-typetypearrays into a single concrete schema withnullablemarking whennullis removed. - Applied the flattening step inside
createToolInstance()for Google/Vertex providers only, after$refresolution and existing schema normalization. - Added unit tests covering union flattening behavior across nested/object/array schemas and nullable/type-array cases.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| packages/api/src/mcp/zod.ts | Adds the Gemini/Vertex-only union-flattening transform for JSON schemas. |
| packages/api/src/mcp/tests/zod.spec.ts | Adds unit tests validating union flattening, nullability handling, and no-op behavior. |
| api/server/services/MCP.js | Gates and applies union flattening only for Google/Vertex tool schema creation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f925783403
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| const rest = { ...current }; | ||
| delete rest[unionKey]; | ||
| current = { ...rest, ...chosen }; |
There was a problem hiding this comment.
Preserve parent fields when flattening unions
When a schema has common object fields plus an anyOf/oneOf branch, this spread order lets the selected branch replace the parent's properties and required arrays. For example, {type:'object', properties:{repo,...}, required:['repo'], anyOf:[{properties:{title:...}, required:['title']}]} flattens to a schema that no longer advertises repo as an argument, so Gemini can omit valid required MCP parameters and the server-side tool call fails even though those fields were outside the union. Merge object properties and combine required entries instead of letting the branch overwrite them wholesale.
Useful? React with 👍 / 👎.
| if (schema && isGoogle) { | ||
| // Gemini/Vertex AI reject union schemas; collapse them so MCP tools that ship | ||
| // unions don't crash the Google endpoint (they work as-is on OpenAI/Claude). | ||
| schema = flattenJsonSchemaUnions(schema); |
There was a problem hiding this comment.
Flatten schemas used for MCP tool definitions
This only flattens the schema passed into the LangChain tool instance; the same MCP tool still carries the raw parameters on mcpJsonSchema, and the definitions-only/deferred-tool path (extractMCPToolDefinition/loadToolDefinitions) builds serializable tool definitions from that field. In event-driven or deferred MCP flows on Google/Vertex, a tool with anyOf/oneOf can therefore still reach the Google converter unflattened and fail with the same union-schema error this change is meant to avoid. Store or propagate the Google-flattened schema for those definitions as well.
Useful? React with 👍 / 👎.
| const { type, nullable } = collapseTypeArray(value); | ||
| if (type !== undefined) { | ||
| result['type'] = type; | ||
| } | ||
| if (nullable) { | ||
| result['nullable'] = true; | ||
| } |
There was a problem hiding this comment.
Remove null enum values when marking nullable
When this handles nullable enum schemas such as { type: ['string', 'null'], enum: ['open', 'closed', null] }, it rewrites the type to string and adds nullable: true but leaves null in the enum. Gemini function schemas only accept homogeneous primitive enum values, so nullable enum parameters from MCP servers can still be rejected even after the union flattening runs. Filter the dropped null out of enum when setting nullable.
Useful? React with 👍 / 👎.
|
Thanks — all three addressed in Finding 1 (preserve parent fields): Finding 3 (null enum values): when a union/ Finding 2 (definitions/deferred path): the flatten was only on the bound tool instance. Now the Google-flattened schema also reaches the definitions path: Coverage: added unit tests for the merge + null-enum cases ( |
|
@codex review |
|
Codex Review: Didn't find any major issues. 🎉 ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
`@langchain/google-common`'s `zod_to_gemini_parameters` throws "Gemini cannot handle union types" on any genuine `anyOf`/`oneOf` (e.g. discriminated unions), so MCP tools shipping union-typed schemas crash on the Google endpoint while working fine on OpenAI/Claude. Add `flattenJsonSchemaUnions` (packages/api) to collapse unions to their first non-null member and multi-entry `type` arrays to a single nullable type, and apply it in `createToolInstance`'s existing `isGoogle` branch so only the Google/Vertex path is affected. Lossy by design, mirroring the existing empty-object fallback. Closes #13612
…r definitions path - Preserve parent-level `properties`/`required` when collapsing a union: merge the chosen branch into the parent instead of overwriting, so args declared outside the union (e.g. always-required fields) still reach Gemini. - Drop the `null` member from `enum` when a union/type-array makes a field nullable, keeping Gemini's required homogeneous-enum invariant. - Propagate the Google-flattened schema to the definitions/deferred-tool path: thread `provider` into `loadToolDefinitions` and flatten there, and store the flattened schema on `mcpJsonSchema` so `extractMCPToolDefinition` no longer emits raw unions on Google/Vertex.
9490ebd to
acfac4e
Compare
The union flatten alone wasn't enough — real GitHub MCP tools on Gemini also 400 with `Invalid value ... (TYPE_STRING), true`, because Gemini's function-calling Schema (https://ai.google.dev/api/caching#Schema) accepts only a restricted JSON Schema subset, and `enum` is `Type.STRING`-only. Rename `flattenJsonSchemaUnions` → `sanitizeGeminiSchema` and broaden it (one pass, Gemini-gated) to cover the documented subset: - Keep only string `enum` values; drop the keyword for non-string types (fixes the reported boolean-enum 400, incl. boolean `const` normalized to `enum: [true]`). - `const` → single-value string enum, or drop if non-string. - Merge `allOf` intersections; fold `exclusiveMinimum`/`exclusiveMaximum` into `minimum`/`maximum`. - Strip unsupported keywords: `additionalProperties`, `default`, `$schema`, `$id`. - (Existing) collapse `anyOf`/`oneOf`, multi-entry `type` arrays, nullable. Grounded in Google's Schema docs rather than reverse-engineered from 400s. Verified end-to-end against the real `@langchain/google-common` converter. Complements danny-avila/agents#232 (langchain bump), which defers schema flattening to LibreChat.
|
Broadened the fix in Gemini's function-calling Schema accepts only a restricted JSON Schema subset, and
Grounded in Google's Schema reference rather than reverse-engineered from 400s, and verified end-to-end against the real |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 16da0fd2d7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (stringValues.length > 0) { | ||
| result['enum'] = stringValues; |
There was a problem hiding this comment.
Drop enums when the effective type is not string
When a MCP schema has a mixed typed enum (for example type: ['integer', 'string'] with enum: [1, 'auto']), the sanitizer collapses the type to the first non-null type but still keeps any string enum values, producing a Gemini schema like type: 'integer', enum: ['auto']. This contradicts the intended “drop the keyword for non-string types” behavior above and can still send Gemini an invalid/misleading enum; the enum should only be retained when the effective sanitized type is string (or when the type is made string).
Useful? React with 👍 / 👎.
Codex review: a mixed-type enum like `type: ['integer','string'], enum: [1,'auto']`
collapsed the type to `integer` but still kept the string value `'auto'`, yielding
`{type:'integer', enum:['auto']}` — a non-string type with an enum, which Gemini
rejects. Keep `enum` only when the effective collapsed type is string (or unset),
and stamp `type: 'string'` on a surviving typeless enum (e.g. a string `const`
discriminator) so it satisfies Gemini's Type.STRING enum requirement.
|
Fixed in |
|
@codex review |
|
Codex Review: Didn't find any major issues. 👍 ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
I fixed a crash where Gemini/Vertex AI agents fail on MCP tools that ship union-typed parameter schemas (e.g. discriminated unions like the GitHub MCP
issue_writetool). These tools work on OpenAI and Claude, but@langchain/google-common'szod_to_gemini_parametersthrows"Gemini cannot handle union types"on any genuineanyOf/oneOf, aborting the run. Resolves #13612.flattenJsonSchemaUnionsinpackages/api/src/mcp/zod.ts, which recursively collapsesanyOf/oneOfto the first non-null member and multi-entrytypearrays (e.g.["string","null"]) to a single type, marking the fieldnullablewhen anullmember is dropped.createToolInstance's existingisGooglebranch inapi/server/services/MCP.js, alongside the empty-object fallback, so only the Google/Vertex path is altered and OpenAI/Claude schemas remain byte-identical.normalizeJsonSchema(which must stay lossless for all providers), since flattening a union is lossy and Gemini-specific.Change Type
Testing
I verified the fix end-to-end against the real
@langchain/google-commonconverter rather than a mock:issue_write-style discriminated-union schema throws the exactzod_to_gemini_parameters: Gemini cannot handle union typeserror today, and is accepted by the same converter afterflattenJsonSchemaUnionsruns.isGooglebranch — so the OpenAI/Claude tool-binding path is unchanged.To reproduce: configure a Google/Vertex Gemini endpoint, attach an MCP server whose tool schema uses
anyOf/oneOf(e.g. the GitHub MCP), and prompt the model to call that tool — previously the run aborted, now it completes.Test Configuration:
cd packages/api && npx jest src/mcp/__tests__/zod.spec.ts— 92/92 passcd api && npx jest server/services/__tests__/MCP.spec.js— 11/11 pass@librechat/apibuilds clean; ESLint clean on all changed filesChecklist